﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExchangeHog.MessageParser
{
	public class ParseException: Exception
	{
		internal ParseException(String message) : base(message) { }
	}
	public class Parser
	{
		Lexer l;
		public Parser(StreamReader sr) { l = new Lexer(sr); }

		public Parser(String s) { l = new Lexer(s); }

		void parseDateTime()
		{
			String dayofweek = null;
			//
			//	[ day-of-week "," ]
			//
			Token t = l.next(Lexer.Mode.DEFAULT);
			if (t.type == Token.Type.ATOM)
				switch (t.repr)
				{
					case "Mon":
					case "Tue":
					case "Wed":
					case "Thu":
					case "Fri":
					case "Sat":
					case "Sun":
						dayofweek = t.repr;
//						l.expectAndAdvance(Token.Type.COMMA);
						break;
				}
			//
			//	date = day month year
			//
			//
			//	time = hour ":" min [":" sec] zone
			//
		}

		//
		//	address
		//		= mailbox/group
		//
		//	where
		//		mailbox 
		//			= name-addr/addr-spec
		//
		//	=
		//		[display-name] angle-addr /
		//		addr-spec /
		//		group
		//	=
		//		[phrase] [CFWS] "<" addr-spec ">" [CFWS] /
		//		addr-spec /
		//		display-name ":" [group-list] ";" [CFWS]
		//	=
		//		[1*word] [CFWS] "<" addr-spec ">" [CFWS] /
		//		addr-spec /
		//		1*word ":" [mailbox-list/CFWS] ";" [CFWS]
		//	=
		//		[1*word] [CFWS] "<" addr-spec ">" [CFWS] /
		//		addr-spec /
		//		1*word ":" [mailbox-list / CFWS] ";" [CFWS]
		//	=
		//		[1*word] [CFWS] "<" addr-spec ">" [CFWS] /
		//		addr-spec /
		//		1*word ":" [(mailbox *("," mailbox)) / CFWS] ";" [CFWS]
		//		
		//	where	
		//
		//	addr-spec	=	(dot-atom / quoted-string ) "@" (dot-atom / [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
		//
		//	where
		//		dtext	= %d33-90 / %d94-126 Printable US-ASCII chars not including "[", "]", "\"

		//
		//	parse phrase/dot-atom/quoted-string prefix that is common to
		//		name-addr
		//		addr-spec
		//		group
		//
		enum ParseLocalPartOrDisplayNameResult { PARSED_DISPLAY_NAME, PARSED_UNCERTAIN } ;
		String parseLocalPartOrDisplayName(out ParseLocalPartOrDisplayNameResult result)
		{
			StringBuilder sbName = new StringBuilder();
			result = ParseLocalPartOrDisplayNameResult.PARSED_UNCERTAIN;
			Token t = l.nextNonCFWS(Lexer.Mode.DEFAULT, Lexer.LEX_ENCODED_WORDS);
			
			while (t.type == Token.Type.ATOM || t.type == Token.Type.DOT_ATOM || t.type == Token.Type.QUOTED_STRING || t.type == Token.Type.ENCODED_WORD)
			{
				if (t.type == Token.Type.ENCODED_WORD)
					result = ParseLocalPartOrDisplayNameResult.PARSED_DISPLAY_NAME;
				sbName.Append(t.repr);
				t = l.nextNonCFWS();
			}
			return sbName.ToString();
		} // end of parseLocalPartOrDisplayName

		String parseLocalPart()
		{
			StringBuilder sbName = new StringBuilder();
			Token t = l.nextNonCFWS();
			while (t.type == Token.Type.ATOM || t.type == Token.Type.DOT_ATOM || t.type == Token.Type.QUOTED_STRING)
			{
				sbName.Append(t.repr);
				t = l.nextNonCFWS();
			}
			return sbName.ToString();
		}

		public interface Addresses
		{
			IEnumerable<Address> addresses();
		}

		public Addresses parseMailboxOrGroup()
		{
			ParseLocalPartOrDisplayNameResult whatDidWeParse;
			String localPartOrDisplayName = parseLocalPartOrDisplayName(out whatDidWeParse);
			Token t = l.current();
			switch (t.type)
			{
				case Token.Type.AT:
					if (whatDidWeParse == ParseLocalPartOrDisplayNameResult.PARSED_DISPLAY_NAME)
						throw new ParseException("Unexpected '@' after display name");
					return new SingleAddress(parseMailbox(localPartOrDisplayName));
				case Token.Type.LESS: // name-addr
					return new SingleAddress(parseMailbox(localPartOrDisplayName));
				case Token.Type.COLON: // group
					return parseGroup(localPartOrDisplayName);
			}
			throw new ParseException("Unexpected token while expecting mailbox or group");
		}

		public class Address
		{
			public String name { get; set; }
			public Address(String name) { this.name = name; }
		}

		class SingleAddress : Addresses
		{
			Address a;
			public SingleAddress(Address a) { this.a = a; }
			public IEnumerable<Address> addresses() { yield return a; }
		}

		String parseAngleAddr(String sDisplayName)
		{
			var sb = new StringBuilder(sDisplayName);
			l.expectCurrent(Token.Type.LESS);
			sb.Append(l.current().repr);
			String angleAddr = parseAddrSpec(parseLocalPart());
			sb.Append(angleAddr);
			l.nextNonCFWS();
			l.expectCurrent(Token.Type.GREAT);
			sb.Append(l.current().repr);
			return sb.ToString();
		} // end of parseAngleAddr

		String parseAddrSpec(String sLocalPart)
		{
			StringBuilder sbAddr = new StringBuilder(sLocalPart); 
			l.expectCurrent(Token.Type.AT);
			sbAddr.Append(l.current().repr);

			//
			//	parse domain
			//
			Token t = l.nextNonCFWS();
			switch (t.type)
			{
				case Token.Type.LBRACKET:
					do
					{
						sbAddr.Append(t.repr);
						t = l.next(Lexer.Mode.DTEXT);
						switch (t.type)
						{
							case Token.Type.WSP: sbAddr.Append(" "); break;
							case Token.Type.DTEXT: sbAddr.Append(t.repr); break;
						}
					} while (t.type != Token.Type.RBRACKET);
					sbAddr.Append(t.repr);
					break;
				case Token.Type.ATOM:
				case Token.Type.DOT_ATOM:
					sbAddr.Append(t.repr);
					break;
			}
			return sbAddr.ToString();
		} // end of parseAddrSpec


		class AddressGroup : Addresses
		{
			string displayName;
			public LinkedList<Address> listAddresses;

			public AddressGroup(string displayName) { this.displayName = displayName; listAddresses = new LinkedList<Address>(); }

			public IEnumerable<Address> addresses() { return listAddresses; }
		}

		AddressGroup parseGroup(String displayName)
		{
			var ag = new AddressGroup(displayName);
			l.expectCurrent(Token.Type.COLON);
			do
			{
				ag.listAddresses.AddLast(parseMailbox());
				if (l.current().type != Token.Type.SEMICOLON)
					l.nextNonCFWS();
				else
					break;
			} while (l.current().type == Token.Type.COMMA);
			l.expectCurrent(Token.Type.SEMICOLON);
			return ag;
		} // end of parseGroup

		Address parseMailbox()
		{
			ParseLocalPartOrDisplayNameResult whatDidWeParse;
			String localPartOrDisplayName = parseLocalPartOrDisplayName(out whatDidWeParse);
			Token t = l.current();
			if (t.type == Token.Type.AT && whatDidWeParse == ParseLocalPartOrDisplayNameResult.PARSED_DISPLAY_NAME)
				throw new ParseException("Unexpected '@' after display name");
			return parseMailbox(localPartOrDisplayName);
		}

		Address parseMailbox(String localPartOrDisplayName)
		{
			String mailbox = null;
			Token t = l.current();
			if (t.type == Token.Type.AT)
				mailbox = parseAddrSpec(localPartOrDisplayName);
			else if (t.type == Token.Type.LESS) // name-addr
				mailbox = parseAngleAddr(localPartOrDisplayName);
			return new Address(mailbox);
		} // end of parseMailbox

		LinkedList<Address> parseMailboxList()
		{
			LinkedList<Address> addrs = new LinkedList<Address>();
			do
			{
				addrs.AddLast(parseMailbox());
				l.nextNonCFWS();
			} while (l.current().type == Token.Type.COMMA);
			return addrs;
		}

		LinkedList<Address> parseAddressList()
		{
			LinkedList<Address> addrs = new LinkedList<Address>();
			do
			{
				IEnumerable<Address> addresses = parseMailboxOrGroup().addresses();
				foreach(Address addr in addresses)
					addrs.AddLast(addr);
				l.nextNonCFWS();
			} while (l.current().type == Token.Type.COMMA);
			return addrs;
		}

		String parseUnstructured()
		{
			l.skipWS();
			Token t = l.next(Lexer.Mode.UNSTRUCTURED);
			return t.repr;
		}

		String parseMsgID() { return parseMsgID(null); }
		String parseMsgID(Token? preconsumedToken)
		{
			//
			// [CFWS] "<" id-left "@" id-right ">" [CFWS]
			// id-left         =   dot-atom-text / obs-id-left
			// id-right        =   dot-atom-text / no-fold-literal / obs-id-right
			// no-fold-literal =   "[" *dtext "]"
			//
			StringBuilder sbMsgId = new StringBuilder();

			Token t = preconsumedToken.HasValue ? preconsumedToken.Value : l.nextNonCFWS();
			l.expectCurrent(Token.Type.LESS);
			sbMsgId.Append(t.repr);
			t = l.nextNonCFWS();
			if (t.type != Token.Type.ATOM && t.type != Token.Type.DOT_ATOM)
				l.expectCurrent(Token.Type.ATOM);
			sbMsgId.Append(t.repr);
			t = l.nextNonCFWS();
			l.expectCurrent(Token.Type.AT);
			sbMsgId.Append(t.repr);
			t = l.nextNonCFWS();
			if (t.type != Token.Type.ATOM && t.type != Token.Type.DOT_ATOM)
			{
				l.expectCurrent(Token.Type.LBRACKET);
				sbMsgId.Append(t.repr);
				do
				{
					t = l.next(Lexer.Mode.DTEXT);
					sbMsgId.Append(t.repr);
				} while (t.type != Token.Type.RBRACKET && t.type != Token.Type.EOS);
			}
			else
			{
				sbMsgId.Append(t.repr);
				t = l.nextNonCFWS();
				l.expectCurrent(Token.Type.GREAT);
				sbMsgId.Append(t.repr);
			}
			return sbMsgId.ToString();
		}

		ContentTypeField parseContentType()
		{
			Token tType = l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
			l.expectCurrent(Token.Type.CONTENT_TYPE_TOKEN);
			l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
			l.expectCurrent(Token.Type.SLASH);
			Token tSubtype = l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
			l.expectCurrent(Token.Type.CONTENT_TYPE_TOKEN);

			List<Tuple<String, String>> parameters = null;
			Token tSemicolonOrCRLF = l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
			parameters = new List<Tuple<String, String>>();
			while (tSemicolonOrCRLF.type == Token.Type.SEMICOLON)
			{
				Token tName = l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
				l.expectCurrent(Token.Type.CONTENT_TYPE_TOKEN);
				l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
				l.expectCurrent(Token.Type.EQUAL_SIGN);
				Token tValue = l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
				if (tValue.type != Token.Type.CONTENT_TYPE_TOKEN && tValue.type != Token.Type.QUOTED_STRING)
					throw new ParseException("Unexpected '" + tValue + "' when content type parameter value token or quoted string were expected");

				String value = tValue.repr.TrimStart();
				parameters.Add(
					new Tuple<string,string>(
						tName.repr.TrimStart(),
						tValue.type == Token.Type.QUOTED_STRING? value.Substring(1, value.Length - 2): value
					));
				tSemicolonOrCRLF = l.nextNonCFWS(Lexer.Mode.CONTENT_TYPE);
			}
			return new ContentTypeField(tType.repr.TrimStart(), tSubtype.repr.TrimStart(), parameters);
		}

        Field parseField()
        {
			Field field = null;
			Token t = l.current();
			switch (t.repr.ToLower())
			{
				case "date:":
					String date = parseUnstructured();
					field = new StringField(Field.Type.DATE, date);
					break; // date-time CRLF
				case "from:": // mailbox-list CRLF
					LinkedList<Address> from = parseMailboxList();
					field = new AddressField(Field.Type.FROM, from);
					l.expectCurrent(Token.Type.CRLF);
					break; 
				case "sender:": break; // mailbox CRLF
				case "reply-to:":
					LinkedList<Address> replyto = parseAddressList();
					field = new AddressField(Field.Type.REPLY_TO, replyto);
					l.expectCurrent(Token.Type.CRLF);
					break; // address-list CRLF
				case "to:":
					LinkedList<Address> to = parseAddressList();
					field = new AddressField(Field.Type.TO, to);
					l.expectCurrent(Token.Type.CRLF);
					break; // address-list CRLF
				case "cc:":
					LinkedList<Address> cc = parseAddressList();
					field = new AddressField(Field.Type.CC, cc);
					l.expectCurrent(Token.Type.CRLF);
					break; // address-list CRLF
				case "bcc:": break; // [address-list/CFWS] CRLF

				//
				//	msg-id = [CFWS] "<" dot-atom-text "@" (dot-atom-text/ ("[" *dtext "]")) ">" [CFWS]
				//
				case "message-id:":
				{
					String msgid = parseMsgID();
					field = new StringField(Field.Type.MESSAGE_ID, msgid);
					l.nextNonCFWS();
					l.expectCurrent(Token.Type.CRLF);
					break; // msg-id CRLF
				}
				case "in-reply-to:":
				{
					LinkedList<String> msgids = new LinkedList<string>();
					Token? preconsumedToken = null;
					while (true)
					{
						String msgid = parseMsgID(preconsumedToken);
						msgids.AddLast(msgid);
						preconsumedToken = l.nextNonCFWS();
						if (l.current().type == Token.Type.CRLF)
							break;
					}
					field = new StringsField(Field.Type.RESENT_MESSAGE_ID, msgids);
					break; // 1*msg-id CRLF
				}
				case "references:":
				{
					LinkedList<String> msgids = new LinkedList<string>();
					Token? preconsumedToken = null;
					while (true)
					{
						String msgid = parseMsgID(preconsumedToken);
						msgids.AddLast(msgid);
						preconsumedToken = l.nextNonCFWS();
						if (l.current().type == Token.Type.CRLF)
							break;
					}
					field = new StringsField(Field.Type.REFERENCES, msgids);
					break; // 1*msg-id CRLF
				}
				case "subject:":
					String subject = parseUnstructured();
					field = new StringField(Field.Type.SUBJECT, subject);
					break; // unstructured CRLF
				case "comments:": break; // unstructured CRLF
				case "keywords:": break; // 1*(atom/quoted-string) *("," 1*(atom/quoted-string)) CRLF

				case "resent-date:": break; // date-time CRLF
				case "resent-from:": break; //  mailbox-list CRLF
				case "resent-sender:": break; //  mailbox CRLF
				case "resent-to:": break; //  address-list CRLF
				case "resent-cc:": break; //  address-list CRLF
				case "resent-bcc:": break; //  [address-list / CFWS] CRLF
				case "resent-message-id:":
				{
					String msgid = parseMsgID();
					field = new StringField(Field.Type.RESENT_MESSAGE_ID, msgid);
					break; //  msg-id CRLF
				}
					//
					// path = angle-addr / ([CFWS] "<" [CFWS] ">" [CFWS])
					//
				case "return-path:": 
					String returnpath = parseUnstructured();
					field = new StringField(Field.Type.RETURN_PATH, returnpath);
					break; // path CRLF
					//
					// received-token = word / angle-addr / addr-spec / domain
					//
				case "received:":
					String received = parseUnstructured();
					field = new StringField(Field.Type.RECEIVED, received);
					break; //  "Received:" *received-token ";" date-time CRLF
				case "mime-version:":  // 1*DIGIT "." 1*DIGIT (ignore comments in parenthesis)
				{
					Token major = l.nextNonCFWS(Lexer.Mode.MIME_VERSION);
					l.expectCurrent(Token.Type.MIME_VERSION_DIGITS);
					l.nextNonCFWS(Lexer.Mode.MIME_VERSION);
					l.expectCurrent(Token.Type.MIME_VERSION_DOT);
					Token minor = l.nextNonCFWS(Lexer.Mode.MIME_VERSION);
					l.expectCurrent(Token.Type.MIME_VERSION_DIGITS);
                    while (true)
                    {
                        Token tail = l.next(Lexer.Mode.MIME_VERSION);
                        if (tail.type == Token.Type.CRLF || tail.type == Token.Type.EOS)
                            break;
                    }
					field = new StringField(Field.Type.MIME_VERSION, major.repr.Trim() + "." + minor.repr.Trim());
					break;
				}
				case "content-type:":
					field = parseContentType();
                    break; 
				case "content-transfer-encoding:":
                    String contentTransferEncoding = parseUnstructured();
                    field = new StringField(Field.Type.CONTENT_TRANSFER_ENCODING, contentTransferEncoding);
                    break;
                case "content-id:": 
                    String contentId = parseUnstructured();
                    field = new StringField(Field.Type.CONTENT_ID, contentId);
                    break;
                case "content-description:":
                    String contentDescription = parseUnstructured();
                    field = new StringField(Field.Type.CONTENT_DESCRIPTION, contentDescription);
                    break;
				case "delivered-to:":
					LinkedList<Address> deliveredto = parseAddressList();
					field = new AddressField(Field.Type.DELIVERED_TO, deliveredto);
					break;
				case "errors-to:":
					LinkedList<Address> errorsto = parseAddressList();
					field = new AddressField(Field.Type.ERRORS_TO, errorsto);
					break;
				case "dkim-signature:":
					String dkimsignature = parseUnstructured();
					field = new StringField(Field.Type.DKIM_SIGNATURE, dkimsignature);
					break;
				default:
					String optional = parseUnstructured();
					field = new StringField(t.repr, optional);
					//
					//	optional-field = 1*(33-57/59-126) ":" unstructured CRLF
					//
					break;
			}
			return field;
        }

		public class Field
		{
			public enum Type { DATE, MESSAGE_ID, SUBJECT, FROM, TO, RESENT_MESSAGE_ID, REFERENCES, REPLY_TO, CC, MIME_VERSION, CONTENT_TYPE, CONTENT_TRANSFER_ENCODING, CONTENT_ID, CONTENT_DESCRIPTION, RETURN_PATH, RECEIVED, DELIVERED_TO, ERRORS_TO, DKIM_SIGNATURE, CUSTOM };
			public Type name { get; set; }
			public String customName { get; set; }

			public object value { get; protected set; }
		}

		public class ContentTypeFieldValue
		{
			public String type { get; set; }
			public String subtype { get; set; }
			public List<Tuple<String, String>> parameters { get; set; }
			public ContentTypeFieldValue(String type, String subtype, List<Tuple<String, String>> parameters)
			{
				this.type = type;
				this.subtype = subtype;
				this.parameters = parameters;
			}
		}

		public class ContentTypeField : Field
		{
			public ContentTypeField(String type, String subtype, List<Tuple<String, String>> parameters)
			{
				this.name = Type.CONTENT_TYPE;
				base.value = new ContentTypeFieldValue(type, subtype, parameters);
			}
			public new ContentTypeFieldValue value { get { return (ContentTypeFieldValue)base.value; } }
		}

		public class StringField: Field
		{
			public StringField(Type name, String value) { this.name = name; base.value = value; }
			public StringField(String customName, String value) { this.name = Type.CUSTOM; this.customName = customName; base.value = value; }
			public new String value { get { return (String)base.value; } }
		}

		public class StringsField : Field
		{
			public StringsField(Type name, LinkedList<String> value) { this.name = name; this.value = value; }
		}

		public class AddressField: Field
		{
			public AddressField(Type name, LinkedList<Address> value) { this.name = name; this.value = value; }
		}

		public class Message
		{
			public ICollection<Field> fields;
			public String body;
			public ICollection<Message> parts;

			public Message(ICollection<Field> fields, String body, ICollection<Message> parts)
			{
				this.fields = fields;
				this.body = body;
				this.parts = parts;
			}
		}

		public Message parseMessage()
		{
			var fields = new LinkedList<Field>();
			l.next(Lexer.Mode.FIELDNAME);
			ContentTypeFieldValue contenttype = null;
			String contenttransferencoding = null;
			while(true)
			{
				Field fld = parseField();
				switch (fld.name)
				{
					case Field.Type.CONTENT_TYPE:
						contenttype = ((ContentTypeField)fld).value;
						break;
					case Field.Type.CONTENT_TRANSFER_ENCODING:
						contenttransferencoding = ((StringField)fld).value;
						break;
				}
				fields.AddLast(fld);
				Token t = l.next(Lexer.Mode.FIELDNAME);
				if (t.type == Token.Type.CRLF || t.type == Token.Type.EOS)
					break;
			}

			if (contenttype == null)
				contenttype = new ContentTypeFieldValue("text", "plain", new List<Tuple<String, String>>(new Tuple<String, String>[] { new Tuple<String, String>("charset", "us-ascii") }));

			Token tBody = l.next(Lexer.Mode.BODY);

			String msgbody = tBody.repr;
			List<Message> parts = null;

			switch (contenttype.type)
			{
				//
				//	Discrete media types
				//
				case "text": // plain, html, richtext, enriched
					if (contenttransferencoding != null && contenttransferencoding.ToLower() == "quoted-printable")
					{
						String charset = contenttype.parameters.Find((Tuple<String, String> t) => t.Item1 == "charset").Item2;
						msgbody = Lexer.decodeQuotedPrintable(charset, msgbody);
					}
					break;
				case "image": // jpeg, gif
				case "audio": // basic
				case "video": // mpeg
				case "application": // octet-stream, PostScript
					break; // do nothing
				//
				//	Composite media types
				//
				case "multipart":
				{
					String boundary = "\r\n--" + contenttype.parameters.Find((Tuple<String, String> t) => t.Item1 == "boundary").Item2;
					List<String> bodies = new List<string>();
					int ndxPrevBoundary = 0;
					int ndxBoundary = tBody.repr.IndexOf(boundary);
					while (ndxBoundary >= 0)
					{
						if (ndxBoundary > ndxPrevBoundary)
						{
							ndxPrevBoundary = tBody.repr.IndexOf("\r\n", ndxPrevBoundary) + 2;
							bodies.Add(tBody.repr.Substring(ndxPrevBoundary, ndxBoundary - ndxPrevBoundary));
						}
						ndxPrevBoundary = ndxBoundary + boundary.Length;
						ndxBoundary = tBody.repr.IndexOf(boundary, ndxPrevBoundary);
					}

					parts = new List<Message>();
					foreach (String body in bodies)
						parts.Add(new Parser(body).parseMessage());

					switch (contenttype.subtype)
					{
						case "mixed":
						case "alternative":
						case "parallel":
							break;
					}
					break;
				}
				case "message":
				{
					switch (contenttype.subtype)
					{
						case "rfc822":
						case "partial":
						case "external-body":
							break;
					}
					break;
				}
			}
			return new Message(fields, msgbody, parts);
		}
	}
}
